package com.mjk.common.core.jdbc.ibatis;


import cn.hutool.core.exceptions.ExceptionUtil;
import com.mjk.common.base.error.BizException;
import com.mjk.common.util.*;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.springframework.core.io.Resource;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
 * mybatis xml 热更新
 * @author mjk
 *
 */
public class IbatisMapperRefresh { 
	
    private final Configuration configuration;// MyBatis配置对象

    @SuppressWarnings({ "rawtypes", "unchecked" })
	public IbatisMapperRefresh(Resource[] mapperLocations, Configuration configuration) {  
        this.configuration = configuration; 
        // 更新资源为自定义StrictMap,方便增量重新加载  
        String[] mapFieldNames = new String[]{  "mappedStatements","resultMaps","keyGenerators","parameterMaps" };  
        for (String fieldName : mapFieldNames){ 
            Map map = (Map) ReflectionUtils.getFieldValue(configuration, fieldName);
            if (!(map instanceof StrictMap)){             	
                Map newMap = new StrictMap(StrUtils.toUpperCaseFirstOne(fieldName) + "collection");
                for (Object key : map.keySet()){
                    try {  
                        newMap.put(key, map.get(key));
                    }catch(IllegalArgumentException ex){  
                        newMap.put(key, ex.getMessage());
                    }  
                }
                ReflectionUtils.setFieldValue(configuration, fieldName, newMap);                
            }  
        }  
        Set<String> moniterDir = new HashSet<>();
        if(ObjectUtils.isNotEmpty(mapperLocations)) {
	        for (Resource mapperLocation : mapperLocations) {
	        	String s = mapperLocation.toString().replace("\\\\", "/");
                // xml文件夹匹配字符串，需要根据需要修改
                String mappingPath = "mapper";
                s = s.substring("file [".length(), s.lastIndexOf(mappingPath) + mappingPath.length());
                if (!s.contains(".jar"))//jar包中的不监控
                    moniterDir.add(s);
	        }
	        //启动目录监控
	        for(String path : moniterDir){
	        	refresh(path);
	        }
        }
      
    } 
    /** 
     * 执行刷新 
     * @param filePath 刷新目录
     * @author ThinkGem
     */
    @SuppressWarnings({ "rawtypes"})  
    private void refresh(String filePath)  {  
    	FileObserver observer = new FileObserver();
    	//检测修改的MAPPER文件
    	observer.fileMonitor(filePath, ".xml",5, new FileObserver.FileListener(){
    		@Override
    		public void onFileChange(File file) {

    			try(
                        InputStream inputStream = new FileInputStream(file)
                ){

    				String resource = file.getAbsolutePath();                 
    	                  
	                // 清理已加载的资源标识，方便让它重新加载。  
	                Field loadedResourcesField = configuration.getClass().getDeclaredField("loadedResources");  
	                loadedResourcesField.setAccessible(true);  
	                Set loadedResourcesSet = ((Set)loadedResourcesField.get(configuration));  
	                loadedResourcesSet.remove(resource);  
    	                  
	                //重新编译加载资源文件。  
	                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputStream, configuration,   
	                        resource, configuration.getSqlFragments());  
	                xmlMapperBuilder.parse();  
	            } catch (Exception e) {
	                throw new BizException("Failed to parse mapping resource: '" + file.getAbsolutePath() + "'"
                            + ExceptionUtil.getMessage(e));
	            }
    		}
    	});
    }  
    
    /** 
     * 重写 org.apache.ibatis.session.Configuration.StrictMap 类 
     * 修改 put 方法，允许反复 put更新。 
     */  
    public static class StrictMap<V> extends HashMap<String, V> {  
  
        @Serial
        private static final long serialVersionUID = -4950446264854982944L;
        private final String name;
  
        public StrictMap(String name, int initialCapacity, float loadFactor) {  
            super(initialCapacity, loadFactor);  
            this.name = name;  
        }  
  
        public StrictMap(String name, int initialCapacity) {  
            super(initialCapacity);  
            this.name = name;  
        }  
  
        public StrictMap(String name) {  
            super();  
            this.name = name;  
        }  
  
        public StrictMap(String name, Map<String, ? extends V> m) {  
            super(m);  
            this.name = name;  
        }  
  
        @SuppressWarnings("unchecked")
        @Override
        public V put(String key, V value) { 
            remove(key);  
            if (containsKey(key)) {  
                throw new IllegalArgumentException(name + " already contains value for " + key);  
            }  
            if (key.contains(".")) {  
                final String shortKey = getShortName(key);  
                if (super.get(shortKey) == null) {  
                    super.put(shortKey, value);  
                } else {  
                    super.put(shortKey, (V) new Ambiguity(shortKey));  
                }  
            }  
            return super.put(key, value);  
        }  
        @Override
        public V get(Object key) {  
            V value = super.get(key);  
            if (value == null) {  
                throw new IllegalArgumentException(name + " does not contain value for " + key);  
            }  
            if (value instanceof Ambiguity sub) {
                throw new IllegalArgumentException(sub.getSubject() + " is ambiguous in " + name
                        + " (try using the full name including the namespace, or rename one of the entries)");  
            }  
            return value;  
        }  
  
        private String getShortName(String key) {  
            final String[] keyparts = key.split("\\.");  
            return keyparts[keyparts.length - 1];  
        }

        protected static class Ambiguity {
            private String subject;

            public Ambiguity(String subject) {
                this.subject = subject;
            }

            public String getSubject() {
                return subject;
            }
        }
    }
}
